﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;

namespace XmlVisualizer.DiagramDesigner
{
    /// <summary>
    /// Connection adorner class.
    /// </summary>
    public class ConnectionAdorner : Adorner
    {
        #region Fields
        private DesignerCanvas canvas;
        private Canvas adornerCanvas;
        private Connection connection;
        private PathGeometry pathGeometry;
        private Connector fixConnector, dragConnector;
        private Thumb sourceDragThumb, sinkDragThumb;
        private Pen drawingPen;
        private DesignerItem hitDesignerItem;
        private VisualCollection visualChildren;
        private Connector hitConnector;
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the ConnectionAdorner class. Sets the canvas and connection. 
        /// </summary>
        /// <param name="canvas">DesignerCanvas to set.</param>
        /// <param name="connection">Connection to set.</param>
        public ConnectionAdorner(DesignerCanvas canvas, Connection connection)
            : base(canvas)
        {
            if (canvas == null)
            {
                throw new ArgumentNullException("canvas");
            }

            if (connection == null)
            {
                throw new ArgumentNullException("connection");
            }

            this.canvas = canvas;
            this.adornerCanvas = new Canvas();
            this.visualChildren = new VisualCollection(this);
            this.visualChildren.Add(this.adornerCanvas);

            this.connection = connection;
            this.connection.PropertyChanged += new PropertyChangedEventHandler(this.AnchorPositionChanged);

            this.InitializeDragThumbs();

            this.drawingPen = new Pen(Brushes.LightSlateGray, 1);
            this.drawingPen.LineJoin = PenLineJoin.Round;

            this.Unloaded += new RoutedEventHandler(this.ConnectionAdorner_Unloaded);
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets the count of visual children.
        /// </summary>
        protected override int VisualChildrenCount
        {
            get
            {
                return this.visualChildren.Count;
            }
        }

        /// <summary>
        /// Gets or sets the hit designer item.
        /// </summary>
        private DesignerItem HitDesignerItem
        {
            get
            {
                return this.hitDesignerItem;
            }

            set
            {
                if (this.hitDesignerItem != value)
                {
                    if (this.hitDesignerItem != null)
                    {
                        this.hitDesignerItem.IsDragConnectionOver = false;
                    }

                    this.hitDesignerItem = value;

                    if (this.hitDesignerItem != null)
                    {
                        this.hitDesignerItem.IsDragConnectionOver = true;
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets the hit connector.
        /// </summary>
        private Connector HitConnector
        {
            get
            {
                return this.hitConnector;
            }

            set
            {
                if (this.hitConnector != value)
                {
                    this.hitConnector = value;
                }
            }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Gets the visual child with the specified index.
        /// </summary>
        /// <param name="index">Index of the child to get.</param>
        /// <returns>Returns specified Visual.</returns>
        protected override Visual GetVisualChild(int index)
        {
            return this.visualChildren[index];
        }
        #endregion
        
        #region Event handlers
        /// <summary>
        /// Invoked on render.
        /// </summary>
        /// <param name="dc">DrawingContext for rendering.</param>
        protected override void OnRender(DrawingContext dc)
        {
            base.OnRender(dc);
            dc.DrawGeometry(null, this.drawingPen, this.pathGeometry);
        }

        /// <summary>
        /// Called to arrange and size the content of the adornerCanvas. 
        /// </summary>
        /// <param name="finalSize">The computed size that is used to arrange the content.</param>
        /// <returns>The size of the control.</returns>
        protected override Size ArrangeOverride(Size finalSize)
        {
            this.adornerCanvas.Arrange(new Rect(0, 0, this.canvas.ActualWidth, this.canvas.ActualHeight));
            return finalSize;
        }

        /// <summary>
        /// Invoked when position of anchor changes.
        /// </summary>
        /// <param name="sender">The object where the event handler is attached.</param>
        /// <param name="e">The event data.</param>
        private void AnchorPositionChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName.Equals("AnchorPositionSource"))
            {
                Canvas.SetLeft(this.sourceDragThumb, this.connection.AnchorPositionSource.X);
                Canvas.SetTop(this.sourceDragThumb, this.connection.AnchorPositionSource.Y);
            }

            if (e.PropertyName.Equals("AnchorPositionSink"))
            {
                Canvas.SetLeft(this.sinkDragThumb, this.connection.AnchorPositionSink.X);
                Canvas.SetTop(this.sinkDragThumb, this.connection.AnchorPositionSink.Y);
            }
        }

        /// <summary>
        /// Invoked when thumb dragging completed.
        /// </summary>
        /// <param name="sender">The object where the event handler is attached.</param>
        /// <param name="e">The event data.</param>
        private void ThumbDragThumb_DragCompleted(object sender, DragCompletedEventArgs e)
        {
            if (this.HitConnector != null)
            {
                if (this.connection != null)
                {
                    if (this.connection.Source == this.fixConnector)
                    {
                        this.connection.Sink = this.HitConnector;
                    }
                    else
                    {
                        this.connection.Source = this.HitConnector;
                    }
                }
            }

            this.HitDesignerItem = null;
            this.HitConnector = null;
            this.pathGeometry = null;
            this.connection.StrokeDashArray = null;
            this.InvalidateVisual();
        }

        /// <summary>
        /// Invoked when thumb dragging started.
        /// </summary>
        /// <param name="sender">The object where the event handler is attached.</param>
        /// <param name="e">The event data.</param>
        private void ThumbDragThumb_DragStarted(object sender, DragStartedEventArgs e)
        {
            this.HitDesignerItem = null;
            this.HitConnector = null;
            this.pathGeometry = null;
            this.Cursor = Cursors.Cross;
            this.connection.StrokeDashArray = new DoubleCollection(new double[] { 1, 2 });

            if (sender == this.sourceDragThumb)
            {
                this.fixConnector = this.connection.Sink;
                this.dragConnector = this.connection.Source;
            }
            else if (sender == this.sinkDragThumb)
            {
                this.dragConnector = this.connection.Sink;
                this.fixConnector = this.connection.Source;
            }
        }

        /// <summary>
        /// Invoked while thumb is being dragged.
        /// </summary>
        /// <param name="sender">The object where the event handler is attached.</param>
        /// <param name="e">The event data.</param>
        private void ThumbDragThumb_DragDelta(object sender, DragDeltaEventArgs e)
        {
            Point currentPosition = Mouse.GetPosition(this);
            this.HitTesting(currentPosition);
            this.pathGeometry = this.UpdatePathGeometry(currentPosition);
            this.InvalidateVisual();
        }
        
        /// <summary>
        /// Invoked when the connection adorner is being unloaded.
        /// </summary>
        /// <param name="sender">The object where the event handler is attached.</param>
        /// <param name="e">The event data.</param>
        private void ConnectionAdorner_Unloaded(object sender, RoutedEventArgs e)
        {
            this.sourceDragThumb.DragDelta -= new DragDeltaEventHandler(this.ThumbDragThumb_DragDelta);
            this.sourceDragThumb.DragStarted -= new DragStartedEventHandler(this.ThumbDragThumb_DragStarted);
            this.sourceDragThumb.DragCompleted -= new DragCompletedEventHandler(this.ThumbDragThumb_DragCompleted);

            this.sinkDragThumb.DragDelta -= new DragDeltaEventHandler(this.ThumbDragThumb_DragDelta);
            this.sinkDragThumb.DragStarted -= new DragStartedEventHandler(this.ThumbDragThumb_DragStarted);
            this.sinkDragThumb.DragCompleted -= new DragCompletedEventHandler(this.ThumbDragThumb_DragCompleted);
        }
        #endregion

        #region Private methods
        /// <summary>
        /// Initializes drag thumbs.
        /// </summary>
        private void InitializeDragThumbs()
        {
            Style dragThumbStyle = this.connection.FindResource("ConnectionAdornerThumbStyle") as Style;

            // source drag thumb
            this.sourceDragThumb = new Thumb();
            Canvas.SetLeft(this.sourceDragThumb, this.connection.AnchorPositionSource.X);
            Canvas.SetTop(this.sourceDragThumb, this.connection.AnchorPositionSource.Y);
            this.adornerCanvas.Children.Add(this.sourceDragThumb);
            if (dragThumbStyle != null)
            {
                this.sourceDragThumb.Style = dragThumbStyle;
            }

            this.sourceDragThumb.DragDelta += new DragDeltaEventHandler(this.ThumbDragThumb_DragDelta);
            this.sourceDragThumb.DragStarted += new DragStartedEventHandler(this.ThumbDragThumb_DragStarted);
            this.sourceDragThumb.DragCompleted += new DragCompletedEventHandler(this.ThumbDragThumb_DragCompleted);

            // sink drag thumb
            this.sinkDragThumb = new Thumb();
            Canvas.SetLeft(this.sinkDragThumb, this.connection.AnchorPositionSink.X);
            Canvas.SetTop(this.sinkDragThumb, this.connection.AnchorPositionSink.Y);
            this.adornerCanvas.Children.Add(this.sinkDragThumb);
            if (dragThumbStyle != null)
            {
                this.sinkDragThumb.Style = dragThumbStyle;
            }

            this.sinkDragThumb.DragDelta += new DragDeltaEventHandler(this.ThumbDragThumb_DragDelta);
            this.sinkDragThumb.DragStarted += new DragStartedEventHandler(this.ThumbDragThumb_DragStarted);
            this.sinkDragThumb.DragCompleted += new DragCompletedEventHandler(this.ThumbDragThumb_DragCompleted);
        }

        /// <summary>
        /// Updates geometry of the connection path.
        /// </summary>
        /// <param name="position">Position of the other end of the connection.</param>
        /// <returns>PathGeometry of the calculated path.</returns>
        private PathGeometry UpdatePathGeometry(Point position)
        {
            PathGeometry geometry = new PathGeometry();

            ConnectorOrientation targetOrientation;
            if (this.HitConnector != null)
            {
                targetOrientation = this.HitConnector.Orientation;
            }
            else
            {
                targetOrientation = this.dragConnector.Orientation;
            }

            List<Point> linePoints = PathFinder.GetConnectionLine(this.fixConnector.GetInfo(), position, targetOrientation);

            if (linePoints.Count > 0)
            {
                PathFigure figure = new PathFigure();
                figure.StartPoint = linePoints[0];
                linePoints.Remove(linePoints[0]);
                figure.Segments.Add(new PolyLineSegment(linePoints, true));
                geometry.Figures.Add(figure);
            }

            return geometry;
        }

        /// <summary>
        /// Hit testing of the connection with possible connectors.
        /// </summary>
        /// <param name="hitPoint">Position of the hit point.</param>
        private void HitTesting(Point hitPoint)
        {
            bool hitConnectorFlag = false;

            DependencyObject hitObject = this.canvas.InputHitTest(hitPoint) as DependencyObject;
            while (hitObject != null &&
                   hitObject != this.fixConnector.ParentDesignerItem &&
                   hitObject.GetType() != typeof(DesignerCanvas))
            {
                if (hitObject is Connector)
                {
                    this.HitConnector = hitObject as Connector;
                    hitConnectorFlag = true;
                }

                if (hitObject is DesignerItem)
                {
                    this.HitDesignerItem = hitObject as DesignerItem;
                    if (!hitConnectorFlag)
                    {
                        this.HitConnector = null;
                    }

                    return;
                }

                hitObject = VisualTreeHelper.GetParent(hitObject);
            }

            this.HitConnector = null;
            this.HitDesignerItem = null;
        }
        #endregion
    }
}
